package com.blog.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blog.Utils.MinioUtils;
import com.blog.Utils.Result;
import com.blog.bean.domain.ShardVideo;
import com.blog.exception.ServiceException;
import com.blog.mapper.ShardVideoMapper;
import com.blog.service.AttachmentService;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author 任浩
 * @version 1.0
 */

@Slf4j
@Service
public class AttachmentServiceImpl implements AttachmentService {

    @Value("${minio.bucket.files}")
    private String imageBucket;

    @Value("${minio.bucket.videofiles}")
    private String videoBucket;

    @Value("${minio.bucket.tempVideo}")
    private String tempBucket;

    @Value("${minio.endpoint}")
    private String ENDPOINT;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private ShardVideoMapper shardVideoMapper;


    /**
     * 上传图片的方法
     * @param file
     * @return
     */
    @Override
    public String uploadImage(MultipartFile file) {
        if(file == null){
            throw new ServiceException("当前上传的图片文件为空");
        }

        //判断我们上传的桶是否存在，如果存在直接上传，不存在生成一个桶
        if(!isExistsBucket(imageBucket)){
            log.info("当前要存放数据的桶，不存在，重新生成一个桶");
            createBucket(imageBucket);
        }

        //得到当前上传文件的原始名字
        String orignName = file.getOriginalFilename();
        String[] split = orignName.split(".");
        //上传文件重新生成一个重复的名字，使用UUID和时间戳实现。
        long timeStamp = System.currentTimeMillis();
        String uploadName = timeStamp + UUID.randomUUID().toString()+".jpg";

        String url = uploadFile(file,imageBucket,uploadName);
        log.info("上传成功返回的地址为: {}",url);
        return url;
    }

    /**
     * 这是断点续传，分片上传视频的方法。
     * @param file
     * @param fileName
     * @param shardingIndex
     * @return
     */
    @Override
    public Result uploadVideo(MultipartFile file, String fileName, Integer shardingIndex) {
        //todo  可以根据上传名字，先判断该文件是否已经存在，不存在继续上传，存在跳过上传。
        log.info("当前上传的fileName为: {}",fileName);
        log.info("分片数为: {}",shardingIndex );

        if(file == null){
            throw new ServiceException("当前上传的视频文件为空");
        }

        //判断当前的桶是否存在
        if(!isExistsBucket(tempBucket)){
            createBucket(tempBucket);
        }

        /**
         * 分片上传生成文件名,该文件名是上传到minio的文件名
         * 在分片上传中，该文件名的生成使用统一规则来生成。
         * ----------该规则使用md5生成一个加密串，并且使用分片的序号来拼接。
         */
        String uploadName = MinioUtils.createShardingObjectName(fileName);
        String mergeName = uploadName+shardingIndex+".mp4";
        log.info("生成的分片上传的名字为: {}",mergeName);
        //然后开始上传该文件到minio
        uploadFile(file,tempBucket,mergeName);

        //该分片上传成功之后，需要把该分片上传了几片记录到数据库中，方便如果网络等错误发生，重新上传不至于全部重新上传。
        if(shardingIndex == 0){
            log.info("该文件分片上传，是第一次上传");
            //说明是第一次上传
            shardVideoMapper.insert(new ShardVideo(null,uploadName,1,0));
        }
        else {
            log.info("该文件分片上传，不是第一次上传");

            QueryWrapper<ShardVideo> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("shard_video_name", uploadName).eq("del",0);
            ShardVideo shardVideo = shardVideoMapper.selectOne(queryWrapper);

            // todo  注意！！！
            /**
             * 这样直接把数量加1，说明前端上传的时候是按照分片顺序来上传的，中间没有跳过
             */
            shardVideo.setShardNum(shardVideo.getShardNum()+1);
            shardVideoMapper.update(shardVideo,queryWrapper);
        }

        return Result.success(true);
    }


    /**
     * 在分片视频上传之前，判断该文件已经上传第几片了，并且返回该分片上传分片的数量。
     * @param fileName
     * @return
     */
    @Override
    public Result judgeVideo(String fileName) {
        log.info("当前判断视频上传的fileName为: {}",fileName);
        /**
         *   生成合并文件之后的名称，此规则也是固定的。
         *   md5加密+ "merge" + ".mp4" 组成
         */
        String mergeName = MinioUtils.createShardingObjectName(fileName) + "merge" + ".mp4";
        //首先检查是否有完整的文件上传了。 如果有直接返回完成上传了，不需要在上传文件。完整返回true
        Boolean objectExist = isObjectExist(videoBucket, mergeName);
        if(objectExist){
            log.info("说明合并文件已经生成，直接返回，不需要再次上传");
            return Result.success(true);
        }

        //首先根据名字到minio判断上传到第几片了。
        String uploadName = MinioUtils.createShardingObjectName(fileName);
        QueryWrapper<ShardVideo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("shard_video_name", uploadName).eq("del",0);
        ShardVideo shardVideo = shardVideoMapper.selectOne(queryWrapper);
        if(shardVideo == null){
            //说明没有上传过，直接返回0
            return Result.success(0);
        }
        log.info("当前文件上传到了第{}片",shardVideo.getShardNum());
        return Result.success(shardVideo.getShardNum());
    }

    /**
     * 合并分片上传
     * @param fileName
     * @param shardingTotal
     * @return
     */
    @Override
    public Result mergeVideo(String fileName, Integer shardingTotal) {
        log.info("当前合并上传的fileName为: {},分片总数为:{}",fileName,shardingTotal);
        //判断当前的桶是否存在
        if(!isExistsBucket(videoBucket)){
            createBucket(videoBucket);
        }

        //1、获取所有分块文件
        List<ComposeSource> sources = new ArrayList<>();
        for (int i = 0 ; i < shardingTotal; i++){
            sources.add(ComposeSource.builder()
                    .bucket(tempBucket)  //分块文件所在桶
                    .object(MinioUtils.createShardingObjectName(fileName) + i +".mp4") //分块文件名称
                    .build());
        }

        /**
         *   生成合并文件之后的名称，此规则也是固定的。
         *   md5加密+ "merge" + ".mp4" 组成
         */
        // 2、进行具体的合并
        String mergeName = MinioUtils.createShardingObjectName(fileName);
        String objectName = mergeName + "merge" + ".mp4";

        //2.1 合并之前，应该先判断文件是否已经合并，如果已经合并，那么直接返回。
        Boolean objectExist = isObjectExist(videoBucket, objectName);
        if(objectExist){
            log.info("合并的文件已经生成了，不需要在合并");
            String url = videoBucket + "/" + objectName;
            return Result.success(url);
        }

        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .sources(sources)
                .bucket(videoBucket) //指定合并后文件在哪个桶里
                .object(objectName) //指定上传文件的名称
                .build();

        try {
            minioClient.composeObject(composeObjectArgs);
        }
        catch (Exception e){
            e.printStackTrace();

            //合并出错之后应该删掉临时数据，重新上传。也应该删除数据库记录的数据。
            log.error("当前合并出错，删除临时文件，建议全部重新上传");
            deleteFile(tempBucket,fileName,shardingTotal);
            QueryWrapper<ShardVideo> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("shard_video_name", mergeName);
            shardVideoMapper.update(new ShardVideo(null,mergeName,shardingTotal,1),queryWrapper);

            throw new ServiceException("当前合并出错，删除临时文件，建议全部重新上传！出错报错: " + e.getMessage());
        }

        //3、合并成功之后，把遗留的分片文件删除。就是把temp桶里面的数据删除。
        deleteFile(tempBucket,fileName,shardingTotal);

        //4、修改数据库，删除分片上传记录信息。
        QueryWrapper<ShardVideo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("shard_video_name", mergeName);
        shardVideoMapper.update(new ShardVideo(null,mergeName,shardingTotal,1),queryWrapper);

        String url = videoBucket + "/" + objectName;
        return Result.success(url);
    }


    /**
     * 判断桶是否存在
     * @param bucketName
     * @return
     */
    public Boolean isExistsBucket(String bucketName){
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        }
        catch (Exception e){
            e.printStackTrace();
            throw new ServiceException(e.getMessage());
        }
    }

    /**
     * 生成桶
     * @param bucketName
     */
    public void createBucket(String bucketName){
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
        catch (Exception e){
            e.printStackTrace();
            throw new ServiceException(e.getMessage());
        }
    }

    /**
     * 向Mioio上传文件
     * @param file
     * @param bucketName
     * @param uploadName
     */
    public String uploadFile(MultipartFile file,String bucketName,String uploadName){
        try {
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(uploadName)
                    .stream(file.getInputStream(),file.getSize(),-1)
                    .build();
            minioClient.putObject(putObjectArgs);

            return bucketName + "/" + uploadName;
        }
        catch (Exception e){
            e.printStackTrace();
            throw new ServiceException(e.getMessage());
        }
    }

    /**
     * 判断桶中某个数据是否存在
     * @param bucketName
     * @param objectName
     * @return
     */
    public Boolean isObjectExist(String bucketName,String objectName){
        try {
            GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(videoBucket)
                    .object(objectName)
                    .build());
            if(getObjectResponse != null){
                return true;
            }
        }
        catch (Exception e){
            e.printStackTrace();
            String msg = "The specified key does not exist.";
            if(!msg.equals(e.getMessage())){
                throw  new ServiceException("查询文件是否合并失败: " + e.getMessage());
            }
        }

        return false;
    }


    /** 删除桶中的临时数据
     * @param bucketName
     * @param objectName
     * @param total
     */
    public void deleteFile(String bucketName,String objectName,Integer total){
        String shardingObjectName = MinioUtils.createShardingObjectName(objectName);

        //total就是来判断是否循环删除
        if(total != null){
            for (int i = 0 ; i < total; i++){
                try {
                    minioClient.removeObject(RemoveObjectArgs.builder()
                            .bucket(bucketName)
                            .object(shardingObjectName + i + ".mp4")
                            .build());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        else {
            RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(shardingObjectName)
                    .build();
            try {
                minioClient.removeObject(removeObjectArgs);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
